Skip to content

chore(deps): bump symphonia 0.5 → 0.6 + harden migration checksums against line-ending drift#53

Merged
InstaZDLL merged 4 commits into
mainfrom
chore/symphonia-0.6-migration
May 18, 2026
Merged

chore(deps): bump symphonia 0.5 → 0.6 + harden migration checksums against line-ending drift#53
InstaZDLL merged 4 commits into
mainfrom
chore/symphonia-0.6-migration

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 18, 2026

Summary

Three related changes bundled together — they fell out of investigating why dependabot PR #47 (chore: bump symphonia from 0.5 to 0.6) was failing CI:

  • symphonia 0.5 → 0.6 migration (3 commits, split for review clarity)
  • db::migration_heal — new self-healing layer for sqlx checksum drift from core.autocrlf regressions on Windows
  • Volume slider — drag was being hijacked by the WebView's native drag fallback (separate, surfaced during manual testing)

Closes #47 (dependabot PR — version bump alone, no migration). Will close that PR once this lands.

symphonia 0.6 migration

symphonia 0.6 ships a "Redesigned all audio primitives" release. The two files that touch the decoder API needed manual changes — SampleBuffer was removed, core::probe moved under core::formats, Decoder split into AudioDecoder etc. Full list in the commit message of chore(deps): bump symphonia from 0.5 to 0.6.

Added the all-meta feature so probe scoring keeps ID3/APE/Vorbis tag detection (tag reading still goes through lofty).

db::migration_heal

Recurring symptom on this branch:

migration <id> was previously applied but has been modified

Investigation: 4 of 5 stored checksums in app.db._sqlx_migrations matched the LF (.gitattributes canonical) hash of their migration file, 1 matched the CRLF variant. The CRLF row was the most recent migration — applied during a brief window where Git for Windows' core.autocrlf=true (system scope) leaked CRLF on first checkout, before .gitattributes normalized the working tree back to LF. The stored hash never updated, so every subsequent boot panicked.

The new module runs before each Migrator::run. For each stored row whose checksum doesn't match the compiled-in migration, it recomputes SHA-384 over the LF and CRLF variants of the embedded SQL; if either matches, it silently rewrites the row to the canonical hash with a tracing::warn. A real SQL change still panics — neither normalization can rescue it.

Confirmed live: at the next boot the heal hook logged healed=1 and Migrator::run proceeded without manual DB surgery.

Volume slider fix

Without preventDefault on pointerdown plus select-none on the track, the WebView interprets the gesture as a text/image drag, steals the pointer-event stream, flips the cursor to "no-drop", and freezes the slider. Surfaced during manual symphonia-validation testing.

Test plan

  • cargo check --all-targets
  • cargo test — 97 passed, 0 failed (93 pre-existing + 4 new for migration_heal)
  • cargo clippy --all-targets --no-deps — clean
  • Manual playback: MP3, FLAC, M4A (AAC), seek, crossfade, ReplayGain, spectrum visualizer, volume slider drag
  • Live verification of db::migration_heal on a real CRLF-poisoned app.db

Summary by CodeRabbit

Notes de publication

  • Améliorations

    • Amélioration de la gestion des événements pointeur dans le contrôle de volume pour une meilleure stabilité des interactions.
    • Robustesse accrue des migrations de base de données avec normalisation automatique des fins de ligne.
  • Mises à jour des dépendances

    • Mise à jour de la bibliothèque de traitement audio vers la version 0.6.
    • Mise à jour de Vite en version 8.x.
  • Documentation

    • Documentation moteur de lecture mise à jour.

Review Change Stack

InstaZDLL added 3 commits May 18, 2026 19:06
symphonia 0.6 redesigned all audio primitives, requiring a manual
migration of the two files that touch the decoder API. Closes the
dependabot bump that failed CI.

API changes:
- core::probe::Hint -> core::formats::probe::Hint
- core::codecs::Decoder -> core::codecs::audio::AudioDecoder
- DecoderOptions -> AudioDecoderOptions
- Probe::format() -> Probe::probe() (returns FormatReader directly)
- CODEC_TYPE_NULL find -> format.default_track(TrackType::Audio)
- track.codec_params (struct) -> codec_params.as_ref().unwrap().audio().unwrap()
- CodecRegistry::make -> make_audio_decoder
- next_packet EOF moved from IoError(UnexpectedEof) to Ok(None)
- packet.track_id() -> packet.track_id (field)
- SampleBuffer<f32> -> Vec<f32> + decoded.copy_to_vec_interleaved
- spec().channels.count() -> .channels().count() (methods now)
- Time::from(Duration) -> Time::try_from_secs_f64

Added the `all-meta` feature so probe scoring keeps ID3/APE/Vorbis
tag detection. Tag reading still goes through lofty.

Validated: 97 unit tests pass + manual playback of MP3/FLAC/M4A AAC,
crossfade, ReplayGain, spectrum visualizer.
Windows users with core.autocrlf=true (Git for Windows installer
default) can land a new .sql migration with CRLF endings in their
working tree for the brief window between checkout and
.gitattributes-driven renormalize. sqlx computes the SHA-384 of
whatever bytes are on disk at apply time and stores it in
_sqlx_migrations.checksum. A later `git pull` or `git add
--renormalize` restores LF, but the stored checksum still points at
the CRLF variant, and every subsequent boot panics with:

  migration <id> was previously applied but has been modified

(no auto-recovery — user has to wipe their data dir).

This module runs before every Migrator::run. For each stored row
whose checksum doesn't match the compiled-in migration, it
recomputes SHA-384 over the LF-only and CRLF-only variants of the
embedded SQL. If either matches, the divergence is line-ending-only
and the stored row is silently rewritten to the canonical hash with
a tracing::warn. A real SQL change still panics, because neither
normalization will rescue it.

Confirmed live: on a machine where 4 of 5 stored checksums matched
LF and 1 (the newest) matched CRLF, the heal hook fixed the lone
row at next boot, logged the warning, and let Migrator::run proceed
without further intervention.

Tests: 4 new unit tests covering LF/CRLF round-trip, lone-CR
preservation, idempotence on LF, and distinct hashes per variant.

CLAUDE.md cross-cutting rule updated.
Without preventDefault on pointerdown plus select-none on the track,
the WebView interprets the gesture as a text/image drag, steals the
pointer-event stream mid-drag, flips the cursor to "no-drop", and
freezes the slider. Adds:

- e.preventDefault() in handlePointerDown to suppress the drag
  fallback
- onDragStart={e => e.preventDefault()} as belt-and-suspenders
- select-none on the track to remove the competing text-selection
  gesture
- onPointerUp / onPointerCancel that releasePointerCapture so the
  capture isn't leaked when the user drops outside the track

ProgressBar has the same structure and the same latent vulnerability,
but no bug has been reported there — leaving it for now, will apply
the same idiom if it surfaces.
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: deps Dependencies scope: docs Docs, README, assets type: chore Maintenance size: xl > 500 lines labels May 18, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Warning

Rate limit exceeded

@InstaZDLL has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 35 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba54f548-aa1a-4b6a-bda7-7b22cd597afe

📥 Commits

Reviewing files that changed from the base of the PR and between bf9b750 and dcf584f.

📒 Files selected for processing (1)
  • src-tauri/src/db/migration_heal.rs
📝 Walkthrough

Walkthrough

Mise à jour majeure de Symphonia 0.5 vers 0.6 avec refactorisation complète des pipelines de décodage audio (analysis et streaming crossfade) pour utiliser la nouvelle API, ajout d'un système d'auto-guérison des migrations SQLx, mise à jour de la documentation et corrections mineures d'interaction pointer.

Changes

Symphonia 0.6 upgrade

Layer / File(s) Summary
Dépendances et documentation
src-tauri/Cargo.toml, README.md, docs/features/playback.md, CLAUDE.md
Version Symphonia mise à jour de 0.5 à 0.6, feature all-meta activée pour détection métadonnées au probe, documentation audio et règles de migrations mises à jour.
Pipeline d'analyse audio
src-tauri/src/analysis.rs
Refactorisation complète du décodage : nouveau probe via get_probe().probe(), décodeur via make_audio_decoder(), remplacement de SampleBuffer<f32> par buffer Vec<f32> interleaved avec spec_captured, gestion erreurs packets réécrite.
Décodeur streaming crossfade
src-tauri/src/audio/crossfade.rs
StreamBackend::Symphonia changements : DecoderAudioDecoder, suppression sample_buf, ajout decoded_interleaved et flag spec_captured. Initialisation lazy Resampler, conversion temps via Time::try_from_secs_f64(), logique packet loop adaptée à API 0.6.
Versions UI et interaction pointer
src/components/views/AboutView.tsx, src/components/player/VolumeControl.tsx
Affichage versions mises à jour (Vite 6.x → 8.x, Symphonia 0.5 → 0.6). VolumeControl ajout handlePointerUp et handlers pointer (onPointerUp, onPointerCancel) pour stabiliser fin d'interaction et prévenir drag parasites.

Migration healing pour sqlx

Layer / File(s) Summary
Implémentation module migration_heal
src-tauri/src/db/migration_heal.rs
Nouveau module avec fonction heal_line_ending_drift() : détection/correction automatique divergences checksums causées uniquement par variations LF/CRLF. Normalisation SQL bidirectionnelle (CRLF→LF, LF→CRLF), calcul SHA-384 comparatif, mise à jour BD checksum si divergence line-ending confirmée, logs warn/info, tests unitaires couvrant round-trips et idempotence.
Intégration base de données
src-tauri/src/db/mod.rs, src-tauri/src/db/app_db.rs, src-tauri/src/db/profile_db.rs, CLAUDE.md
Module export et appels intégration : app_db.rs et profile_db.rs exécutent heal_line_ending_drift() avant migrator.run(). Documentation CLAUDE.md mise à jour avec description du mécanisme d'auto-guérison et conditions d'échec.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

type: fix, size: xl, scope: backend, scope: frontend, scope: docs

Poem

📻 Symphonia monte en version,
Le décodage se refait une jeunesse,
LF et CRLF font la paix,
Le volume s'affirme sans trembler,
Une mise à jour qui résonne juste. 🎵

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit les conventions Conventional Commits avec un scope kebab-case approprié et décrit avec clarté les deux changements majeurs du PR (bump symphonia et renforcement des checksums de migration).
Description check ✅ Passed La description couvre tous les points essentiels : résumé des trois changements, raison du bundling, détails techniques des migrations API Symphonia 0.6, mécanisme de self-healing des checksums, et plan de test complet avec validation manuelle et cargo checks.
Linked Issues check ✅ Passed Les modifications satisfont l'objectif principal du PR #47 : bump de symphonia 0.5 → 0.6 avec mise à jour du code pour les breaking changes (refactorisation SampleBuffer, API probe/decoder), ajout de all-meta feature, et validation exhaustive (97 tests passants, playback manuel sur MP3/FLAC/M4A, cargo checks/clippy clean).
Out of Scope Changes check ✅ Passed Le changement du VolumeControl (drag hijack fix) est justifié comme surfacé lors du test manual de symphonia et est un correctif réel aux événements pointer, non une déviation. Tous les changements servent les trois objectifs déclarés : migration, migration_heal, et volume fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/symphonia-0.6-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this May 18, 2026
Per CodeRabbit review on #53: the previous +1/32 margin tripped
reallocations on every realistic migration (typical SQL is 5–10 %
`\n`), while a blanket ×2 would waste roughly half the allocation.
A linear pre-pass over a few KB of bytes is free.
@InstaZDLL InstaZDLL enabled auto-merge May 18, 2026 17:16
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src-tauri/src/analysis.rs (1)

125-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gérer IoError et ResetRequired dans decoder.decode() comme dans next_packet().

La ligne 131 échoue fatalement l'analyse sur tout SymphoniaError hors DecodeError. Or Symphonia 0.6 peut retourner IoError (fichier tronqué, flux corrompu) et ResetRequired (changement de spec) en décodage de paquet. Un fichier légèrement corrompu ou tronqué va faire échouer toute l'analyse alors que le reste du flux est exploitable. Le code gère déjà ResetRequired ligne 118 pour next_packet() — il faut appliquer la même logique au décodeur.

💡 Correctif proposé
         let decoded = match decoder.decode(&packet) {
             Ok(d) => d,
+            Err(SymphoniaError::IoError(e)) => {
+                tracing::warn!(error = %e, "analysis: io error, skipping packet");
+                continue;
+            }
             Err(SymphoniaError::DecodeError(e)) => {
                 tracing::warn!(error = %e, "analysis: decode error, skipping packet");
                 continue;
             }
+            Err(SymphoniaError::ResetRequired) => break,
             Err(e) => return Err(format!("decode: {e}")),
         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/analysis.rs` around lines 125 - 131, decoder.decode currently
treats any SymphoniaError other than DecodeError as fatal (returning Err), but
SymphoniaError can be IoError or ResetRequired which should be handled like
next_packet() does; update the match in analysis.rs around decoder.decode to
handle Err(SymphoniaError::IoError(e)) by logging a warning and continue, and
Err(SymphoniaError::ResetRequired) by performing the same reset logic used for
next_packet() (reset the decoder/track processing and continue) instead of
returning an error, keeping the existing warning/skip behavior for DecodeError
and only returning Err for truly unrecoverable/unexpected variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/audio/crossfade.rs`:
- Around line 298-305: The call to Time::try_from_secs_f64 doesn't exist in
symphonia-core 0.6; replace it by converting ms to seconds as an f64 and using
the available constructor/trait: either call Time::from(seconds_f64) (From<f64>
impl) or build with Time::new(seconds_u64, frac_f64) derived from the fractional
seconds; keep the same fallback behavior (use Default or zero time if conversion
somehow fails) and update the expression that assigns `time` (currently using
Time::try_from_secs_f64 and `ms`) accordingly.
- Around line 139-150: The call to format.default_track(TrackType::Audio) is
invalid in Symphonia 0.6; change the call to format.default_track() (remove the
TrackType::Audio argument) to get the default Track, then keep the existing
audio parameter validation via
track_symphonia.codec_params.as_ref().and_then(|p| p.audio()).ok_or_else(...)
and the decoder creation using
symphonia::default::get_codecs().make_audio_decoder(...).

---

Outside diff comments:
In `@src-tauri/src/analysis.rs`:
- Around line 125-131: decoder.decode currently treats any SymphoniaError other
than DecodeError as fatal (returning Err), but SymphoniaError can be IoError or
ResetRequired which should be handled like next_packet() does; update the match
in analysis.rs around decoder.decode to handle Err(SymphoniaError::IoError(e))
by logging a warning and continue, and Err(SymphoniaError::ResetRequired) by
performing the same reset logic used for next_packet() (reset the decoder/track
processing and continue) instead of returning an error, keeping the existing
warning/skip behavior for DecodeError and only returning Err for truly
unrecoverable/unexpected variants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e7988a35-da23-41d2-963c-5c790bc5c62e

📥 Commits

Reviewing files that changed from the base of the PR and between 41d1a60 and bf9b750.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock, !src-tauri/Cargo.lock
📒 Files selected for processing (13)
  • CLAUDE.md
  • README.md
  • docs/features/playback.md
  • src-tauri/Cargo.toml
  • src-tauri/src/analysis.rs
  • src-tauri/src/audio/crossfade.rs
  • src-tauri/src/audio/dsd/mod.rs
  • src-tauri/src/db/app_db.rs
  • src-tauri/src/db/migration_heal.rs
  • src-tauri/src/db/mod.rs
  • src-tauri/src/db/profile_db.rs
  • src/components/player/VolumeControl.tsx
  • src/components/views/AboutView.tsx

Comment thread src-tauri/src/audio/crossfade.rs
Comment thread src-tauri/src/audio/crossfade.rs
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai Findings rejected after verification against the actual installed symphonia-core-0.6.0 source. Details below for the next reviewer.

Finding 1 (Critical) — Time::try_from_secs_f64 allegedly missing

Verified in ~/.cargo/registry/.../symphonia-core-0.6.0/src/units.rs:580:

pub fn try_from_secs_f64(total_secs: f64) -> Option<Time> { ... }

The method exists. Additional evidence:

  • cargo check --all-targets
  • cargo clippy --all-targets --no-deps ✓ (and clippy itself suggested unwrap_or_default() on this exact call — impossible if the method didn't exist)
  • Seeking was manually tested on FLAC / MP3 / M4A.

Finding 2 (Critical) — default_track allegedly takes no argument

Verified in ~/.cargo/registry/.../symphonia-core-0.6.0/src/formats/mod.rs:630:

fn default_track(&self, track_type: TrackType) -> Option<&Track> { ... }

The method takes a TrackType. The current code wouldn't compile without it. The Context7-fetched canonical example (https://context7.com/pdeljanov/symphonia/llms.txt) also uses this exact form:

let track = format.default_track(TrackType::Audio).unwrap();

Finding 3 — decoder.decode error matching pre-existing

The error-matching arms around decoder.decode in analysis.rs are byte-for-byte identical to the pre-migration 0.5 code — this PR didn't touch them. Treating IoError-at-decode as fatal during a one-shot analysis pass is defensible (unrecoverable read mid-decode) and unrelated to the symphonia 0.6 migration. Hardening the pattern is a worthwhile follow-up but out of scope here.

Both Critical findings appear to be hallucinated API shapes — possibly from a pre-release symphonia 0.6 doc snapshot. Worth flagging upstream.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL : merci pour la vérification rigoureuse et les preuves directement tirées des sources installées. Vous avez totalement raison sur les trois points.

  • Finding 1 : Time::try_from_secs_f64 existe bien dans symphonia-core 0.6.0. La passe cargo clippy qui suggère .unwrap_or_default() sur ce call en est la preuve irréfutable.
  • Finding 2 : default_track prend bien un argument TrackType dans l'API 0.6.0 — le code ne compilerait tout simplement pas sans cela.
  • Finding 3 : Les bras de matching autour de decoder.decode sont antérieurs à ce PR, donc hors scope.

Ces findings étaient des hallucinations de ma part, vraisemblablement basées sur un snapshot de doc pré-release. Je m'en excuse.

Voulez-vous que je résolve mes précédents commentaires de review incorrects sur cette PR ?

🐇💦


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/53

Timestamp: 2026-05-18T17:21:03.586Z
Learning: In symphonia-core 0.6.0, `Time::try_from_secs_f64(total_secs: f64) -> Option<Time>` exists at `units.rs:580`. Previous reports of this method being absent in 0.6.0 were incorrect hallucinations.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/53

Timestamp: 2026-05-18T17:21:03.586Z
Learning: In symphonia-core 0.6.0, `FormatReader::default_track` takes a `TrackType` argument: `fn default_track(&self, track_type: TrackType) -> Option<&Track>`. The canonical usage is `format.default_track(TrackType::Audio)`. Previous reports claiming it takes no argument were incorrect hallucinations.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/53

Timestamp: 2026-05-18T17:21:03.586Z
Learning: In symphonia 0.6.0 (WaveFlow project, src-tauri/src/audio/crossfade.rs and src-tauri/src/analysis.rs), the decoder type is `Box<dyn AudioDecoder>` (not `Box<dyn Decoder>` as in 0.5), and `copy_to_vec_interleaved` replaces `SampleBuffer`. These are the correct 0.6.0 API shapes verified against the installed crate source.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@InstaZDLL InstaZDLL merged commit d0c7aa9 into main May 18, 2026
13 checks passed
@InstaZDLL InstaZDLL deleted the chore/symphonia-0.6-migration branch May 18, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: deps Dependencies scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) size: xl > 500 lines type: chore Maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant